#!/usr/bin/env python
# -*- coding: UTF-8 -*-

'''Implementation of http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life'''

from cellular_engine.engine import TileManager

import random

# Playing around with these is like playing god.
RULE_CELLS_TO_STAY = 2  # Less than this and a cell dies
RULE_CELLS_TO_LIVE = 3  # This amount to become a live cell
RULE_CELLS_TO_DIE = 4  # >= This and a cell dies

# How much of the possible tiles we should fill when we do a random seed
SEED_PERCENTAGE = 0.12


class GameOfLife(object):
    def __init__(self, world_size, tile_size, world_type):
        self.alive_cells = set()
        self.generation = 0

        self.manager = TileManager(world_size, tile_size, world_type)
        self.get_coords_around = self.manager.get_coords_around

    def export_dictionary(self):
        '''Return a dictionary that can be serialized to restore the state'''
        attr = {'cells': self.alive_cells,
                'gen': self.generation,
                'world': self.manager.world_size,
                'tile': self.manager.tile_size,
                'wtype': self.manager.world_type}

        return attr

    @classmethod
    def import_dictionary(cls, attr):
        '''Initialize a instance from the value of the export_attributes method'''
        world_size = attr['world']
        tile_size = attr['tile']
        world_type = attr['wtype']

        instance = cls(world_size, tile_size, world_type)

        instance.alive_cells = attr['cells']
        instance.generation = attr['gen']

        return instance

    def _get_alive_neighbours(self, position):
        '''Return the amount of alive neighbors next to a cell.

        The iteration stops if the amount of alive neighbors reaches
        RULE_CELLS_TO_DIE'''
        alive = 0

        alive_cells = self.alive_cells
        for coord in self.get_coords_around(position):
            if coord in alive_cells:
                alive += 1

                # Any check past this point is a waste of computation time
                if alive == RULE_CELLS_TO_DIE:
                    break

        return alive

    def _check_alive_cells_for_death(self):
        '''Return a set with all cell coordinates that are ready to die'''
        cells_to_kill = set()

        _get_alive_neighbours = self._get_alive_neighbours
        for cell in self.alive_cells:
            neighbours = _get_alive_neighbours(cell)

            if neighbours < RULE_CELLS_TO_STAY or neighbours >= RULE_CELLS_TO_DIE:
                cells_to_kill.add(cell)

        return cells_to_kill

    def _check_dead_cells_for_spawn(self):
        '''Return a list of cell coordinates that are ready to live'''
        alive_neighbours = {}
        alive_cells = self.alive_cells

        # Only check cells adjacent to alive cells, since there's no chance
        # for life to spring up in a dead area
        get_coords_around = self.get_coords_around
        for cell in alive_cells:
            for new_position in get_coords_around(cell):
                # If a alive cell finds a dead cell adjacent to it we increase
                # the amount of alive cells that dead cells knows by one
                if new_position not in alive_cells:
                    x, y = new_position

                    # WORLD_TYPE_DEAD_AROUND gives impossible values
                    if x < 0 or y < 0:
                        continue

                    try:
                        alive_neighbours[new_position] += 1
                    except KeyError:
                        alive_neighbours[new_position] = 1

        cells_to_live = [cell for cell in alive_neighbours
                         if alive_neighbours[cell] == RULE_CELLS_TO_LIVE]

        return cells_to_live

    def toggle_state_at_position(self, position):
        position = self.manager.get_cell_pos_from_point(position)

        try:
            self.alive_cells.remove(position)
        except KeyError:
            self.alive_cells.add(position)

    def game_loop(self):
        '''Do the main loop for Conways Game of Life.
           Kill stuff, spawn stuff, repeat'''
        cells_to_kill = self._check_alive_cells_for_death()
        cells_to_live = self._check_dead_cells_for_spawn()

        self.alive_cells.difference_update(cells_to_kill)
        self.alive_cells = self.alive_cells.union(cells_to_live)

        self.generation += 1

    def reset(self):
        '''Set everything back to square 0'''
        self.alive_cells = set()
        self.generation = 0

    def new_seed(self, percentage_to_fill=SEED_PERCENTAGE):
        '''Applys a random seed on the map'''
        if percentage_to_fill < 0.0 or percentage_to_fill > 1.0:
            raise ValueError('Invalid percentage. Must be between 0.0 and 1.0')
        self.reset()

        world_size = self.manager.world_size
        tile_size = self.manager.tile_size

        world_x = world_size[0]
        world_y = world_size[1]

        tile_x = tile_size[0]
        tile_y = tile_size[1]

        # How much tiles we could fill in total
        max_cells = world_x / tile_x * world_y / tile_y
        cells_to_build = max_cells * percentage_to_fill

        print('Applying a new seed! (%i alive cells)' % cells_to_build)

        while len(self.alive_cells) < cells_to_build:
            x = random.choice(range(0, world_x, tile_x))
            y = random.choice(range(0, world_y, tile_y))

            self.alive_cells.add((x, y))
